/*
 * jDataStore Javascript Library v.0.1a
 * http://code.google.com/p/jdatastore/
 *
 * Copyright (c) 2009 Nicolas Rudas Licenced under the MIT Licence
 *
 * Date: $Date: 2009-02-27 18:06:01 +0000 (Fri, 27 Feb 2009) $
 * Revision: $Rev: 31 $
 */

// Debugging fns
var debug = false;
var time = function(name,args) {
	if(!debug){return;}
	var self = time;
	self.timers = self.timers || {};
	self.timers[name] =  {
		time : new Date().valueOf(),
		args : args	};
};
var timeEnd = function(name,args) {
	if(!debug){return;}
	
	var self = window.time;
	self.timers = self.timers || {};
	args = (args) ?  ' (' + args +')' : '';
	var timeargs = (self.timers[name].args) ? ': ' + self.timers[name].args : '';
	var time = name + timeargs +'\n\t'+(new Date().valueOf() - self.timers[name].time)/1000;
	
	if(window.console) { console.log(time + args);}
	//else {$('body').append('<p>'+time+': '+args+'</p>');}
	
	self.timers[name] = {};
};

;(function() {
	var window = this,
		rand = function() {
		return parseInt((new Date().valueOf()+Math.floor(Math.random()*new Date().valueOf()))/(Math.floor(Math.random()*9)+1),10);
	};
		
	var Store = window.Store = function(data,opts,exts) {
		if(!data) { return null;}
		return new Store.item.init(data,opts,exts);
	};
	
	var _utils = Store.utils = {
	// ** RegExp helpers **
		regexp : {
			escape: function(s) { return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1'); }
		},
		
		filter : function(what,where) {

			var sameArrays = function(arr,arr1) {
				var al = arr.length, bl = arr1.length;
				if (al !== bl || al === 0 || bl === 0) { return; }
				for (var i = al - 1; i >= 0; i--){
					if (!match(arr[i], arr1[i])) { return; }
				}
				return arr1;
			};

			var sameObjects = function(obj,obj1) {
				for (var i in obj) {
					if(!obj1.hasOwnProperty(i)){return;}
					if (!match(obj[i],obj1[i])) {return;}
				}
				return obj1;
			};

			var inArray = function(value,obj) {
				var r = [],
					m = match;

				for (var i = obj.length - 1; i >= 0; i--){
					var item = obj[i];
					if( m(value,item) ) {
						r.push( item );
					}
				};

				return (r.length) ? r : undefined ;
			};

			var inObject = function(value,obj) {
				var r = [],
					m = match;

				for(var i in obj){
					var item = obj[i];
					if( m(value,item) ) {
						r.push( item );
					}
				};

				return (r.length) ? r : undefined ;
			};

			var inString = function(value,string) {	
				var	valueConstructor = value.constructor;

				if( valueConstructor === RegExp) {
					return (value.test(string)) ? string : undefined ; }

				else if( valueConstructor === Function && value(string) === true ) {
					return string; }

			};

			var match = function(a,b) {
				if(typeof a === 'undefined' || typeof b === 'undefined' || a === null || b === null) {
					return; }

				if (a == b) { 
					return b;}

				var	vc = a.constructor,
					oc = b.constructor;

			// Cannot compare array or object to a string or a number
				if( (vc === Array || vc === Object) && (oc === String || oc === Number) ) {
					return;	}

				if( oc === Array && vc === Array ) {
					return sameArrays(a,b);	}
				else if(oc === Array) {
					return inArray(a,b); }
				else if (oc === Object && vc === Object) {
					return sameObjects(a,b); }
				else if (oc === Object) {
					return inObject(a,b); }			
				else if (vc === Object || vc === Array) {
					return;	}
				else if (oc === String || oc === Number){
					return inString(a,b); }

			};

			return match(what,where);
		},
		
	// ** Map ** 
	// Applies callback function for each element in the object passed,
	// and returns object of values that this function returned
	// If function returns FALSE or UNDEFINED, those values are not included
	// in returned object
		map: function(obj,fn,fn2){
			var fn_return = [],
				result;
		
			if(obj.length) {
			//	obj.reverse();
				for (var i = obj.length - 1; i >= 0; i--){
					result = fn.apply(this,[obj[i],i]);
					if(result === false || typeof result === 'undefined'){ continue; }
					if(result.constructor === Array) {
						fn_return = fn_return.concat(result);}
					else {
						fn_return.push(result); }	};
			//	fn_return.reverse();
			}
			else {
				for(var i in obj) {
					result = fn.apply(this,[obj[i],i]);
					if(result === false || typeof result === 'undefined'){ continue; }
					
					if(result.constructor === Array) {
						fn_return = fn_return.concat(result);}
					else {
						fn_return.push(result); }
				}
			}
			if(fn2){
				return fn2.apply(this,[fn_return]); }
		
			return fn_return;
			
		},
	// ** Each ** 
	// Applies callback function for each element in the object passed,
	// and returns object of values that this function returned
	// If function returns FALSE loop is terminated and results returned;
		each : function(obj,fn,fn2) {
			var fn_return = [],
				result;
		
			if(obj.length) {
				obj.reverse();
				for (var i = obj.length - 1; i >= 0; i--){
					result = fn.apply(this,[obj[i],i]);
					if(result === false){ break; }
					fn_return.push(result); }; obj.reverse(); }
			else {
				for(var i in obj) {
					result = fn.apply(this,[obj[i],i]);
					if(result === false){ break; }
					fn_return.push(result); }; }	

			if(fn2){
				return fn2.apply(this,[fn_return]); }
		
			return fn_return;
		},
	
	// jQuery extend
		extend : function() {
			// copy reference to target object
			var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;
			
			var _extend = this.extend;
						
			// Handle a deep copy situation
			if ( typeof target === "boolean" ) {
				deep = target;
				target = arguments[1] || {};
				// skip the boolean and the target
				i = 2;
			}

			// Handle case when target is a string or something (possible in deep copy)
			if ( typeof target !== "object" && typeof target !== 'function' )
				target = {};

			// extend jQuery itself if only one argument is passed
			if ( length == i ) {
				target = this;
				--i;
			}

			for ( ; i < length; i++ )
				// Only deal with non-null/undefined values
				if ( (options = arguments[ i ]) != null )
					// Extend the base object
					for ( var name in options ) {
						var src = target[ name ], copy = options[ name ];

						// Prevent never-ending loop
						if ( target === copy )
							continue;

						// Recurse if we're merging object values
						if ( deep && copy && typeof copy === "object"  )
							target[ name ] = _extend( deep, 
								// Never move original objects, clone them
								src || ( copy.length != null ? [ ] : { } )
							, copy );

						// Don't bring in undefined values
						else if ( copy !== undefined )
							target[ name ] = copy;

					}

			// Return the modified object
			return target;
		},
	
	
	// Find helpers
		not: function(what) {
			return function(value) {
				if(what.constructor !== RegExp) {
					what = what.toString();
					what = new RegExp('^'+Store.utils.regexp.escape(what)+'$' );
				}
				
				return (what.test(value)) ? false : true;
			};			
		},
		gt: function(than) {
			return function(value) {
				return value > than;
			};
		},
		gte: function(than) {
			return function(value) {
				return value >= than;
			};
		},
		lt: function(than) {
			return function(value) {
				return value < than;
			};
		},
		lte: function(than) {
			return function(value) {
				return value <= than;
			};
		},
		range: function(start,end) {
			return function(value) {
				return (value >= start && value <= end); 
			};
		}
	};
	
	Store.item = {
	// Initialise a storage item
	// Data must be an array (of objects, key/value pairs)
		init : function(data,opts) {
		
		// Used for .end method
		// returns the previous object
			this._prevObj = false; //this;
		
		// Flag this object as a Store object
			this.store = true;
		
		// Search results have selector attr
		// which gets passed the search inputs (what was the search for)
			this.selector = false;
	
		// A place to save events
			this.events = {
				_properties : {
					target : null,
					type: null,
					before : null,
					selector : null,
					result : null,
					params : null
				}
			};
			
		// Save the data
			this.push(data);
	
		// Assign a unique ID
			this.id = rand();
		
		// A place to save search results
			this.__search_results = {};

		// A place to save some settings	
			this.settings = {};		
			
			return this;
		
		},
		end: function(){ // OK!
			return this._prevObj || Store([]);
		},
		root : function() { // OK!
			var r = this;
		
			do { r = r.end(); }
			while(r.end().data.length);
		
			return r;
		},

		eq: function(index){ // OK!
			var n = Store([this.data[index]],this.settings); 
		
			n._prevObj = this;
		
			return n;
		},
		slice : function(a) { // OK?
			var r;
		
		// Returned slice matches array of indeces
			if(a && a.constructor == Array) {			
				r = _utils.map.call(this,a,function(item) {
					return this.data[item]; });
										
				r = Store(r,this.settings); }
			
		// Returned slice like native array slice (by start and end indeces)
			else {
				r = Store(Array.prototype.slice.apply(this.data,arguments),this.settings); }
		
			r._prevObj = this;
		
			return r;
		},

		push: function(newdata) { // OK!
			this.data = this.data || [];
			
		// Push objects of array	
			if(newdata.constructor == Array && newdata.length) {			
				this.data = this.data.concat(newdata);}				
		// Push objects from Store object	
			else if(newdata.constructor == Object && newdata.data && newdata.store) {
				this.push( newdata.data );		
				return this; }
		// Push an object
			else if (newdata.constructor == Object) {
				this.data.push( newdata ); }
		
		// Correct length
			this.length = this.data.length;
		
		// Trigger events
			this.trigger('push',null,{
				target : this,
				params : (newdata.constructor === Array) ? newdata : [newdata]
			});
				
			return this;
		
		},
		insert: function(obj){return this.push(obj);},
		update: function(what){ // OK!
			var updateIndex, searchable = this.settings.searchable,
				args = Array.prototype.slice.apply(arguments);
		
		// Convert values from ('key','value') to ({ key : value })
			if(args.length == 2
				&& typeof args[0] == 'string' && typeof args[1] !== 'undefined'
				&& _utils.filter(args[1].constructor,[ String, Number, RegExp ])
			){
				var k = args[0],v = args[1];what = {};what[k] = v; }
		
			var keys = 0; // remove this
		
			time('Update');
					
			this.each(function(item,index) {
				for(var key in what) {
					var storedValue = item[key],
						newValue = what[key];
				// Update only, do not create new values	
					if(typeof storedValue === 'undefined' || newValue === storedValue) {continue;}

					keys++; // remove this
				
					newValue = (newValue.constructor === Function)
									? newValue(storedValue)
									: ( newValue.constructor === RegExp && storedValue.constructor === String )
										? storedValue.match(newValue)
										: newValue;
				
					item[key] = newValue;};
			});
			
			timeEnd('Update',this.length+'/'+this.end().length+' record(s), '+keys+' attribute(s)');
			
		// Trigger events
			this.trigger('update',null,{
				target : this,
				params : what
			});
						
			return this;
		},
		remove: function(){ // OK!
			var root = this.root();
		
			time('Remove');
		
		// Mark each item as to be deleted	
			this.each(function(item,i) {
				item.__delete = true; });
						
		// Find these items in root and delete them
			root.each(function(item,index) {
				if(item.__delete) {
					this.data.splice(index,1); } });
		
		// Make sure root has correct length property			
			root.length = root.data.length;

			timeEnd('Remove');
			
		// Trigger events
			this.trigger('remove',null,{
				target : this
			});

		// Return a now empty and not very useful this
			this.data = [];
			this.length = 0;
		
			return this;
		},

		print: function(key,template){ // OK!
			if(!key){return [];}
			var r;
		
			key = (key.constructor == Array) ? key : [key];
	
		//	If no template provided, just join data with a coma	
			template = template || (function() {
				return _utils.map(key,function(item,index) {
					return '$'+(index+1);
				},function(r) {
					return r.reverse().join(', '); });
			})();
	
		//	Apply template to data
		//	Each key can be referred to by using $number,
		//	where number is the key's index in the array (key param)
			this.each(function(item,index) {
				var r = template;
				for (var i = key.length - 1; i >= 0; i--){
					r = r.replace( new RegExp( '\\$' + (i + 1) , 'g') ,item[key[i]] || '');
				};
				return r.replace(/\$0/g,index + 1);
			},function(items){ r=items.reverse(); });
			
			return r;
		},

		each: function(fn,fn2){ // OK!
			fn = (typeof fn == 'function') ? fn : false;
		
			if(!fn || !this.data){
				return this;}
			
			return _utils.each.apply(this,[this.data,fn,fn2]);
		},
	
		makeUnique: function(){
			var d = this.data.reverse(),
				n = this.data = [],
				filter = _utils.filter;
			
			time('makeUnique',d.length);
			
			this.data = _utils.map(d, function(item) {	
				if( !filter(item,n) ){
					n.push(item);
					return item;
				}
			});
			
			timeEnd('makeUnique',this.data.length);
			
			this.length = this.data.length;

		// Trigger events
			this.trigger('makeUnique',null,{
				target : this,
				params : d.reverse(),
				result: n
			});
			
			return this;
		},
		
		bind: function(e,fn){
			var self = this;
			
			var events = this.events[e] = this.events[e] || [];
			(function() {
				events.push(function() {
					return fn.apply(self,Array.prototype.slice.apply(arguments));
				});
			})();
			return this;	
		},
		trigger: function(e,data,params){
			var events = this.events[e];
			if(!events) {return this;}
			
			params = params || {};
			params.type = params.type || e;
			
			var	props = _utils.extend({},this.events._properties,params);
			
			_utils.each(events,function(item) {
				item(props,data);
			});
			
			return this;
		},

		find: function() {
			var searchFor = Array.prototype.slice.apply(arguments);			
	
		// Convert search values from ('key','value') to ({ key : value })
			if(searchFor.length == 2
				&& typeof searchFor[0] === 'string' && typeof searchFor[1] !== 'undefined'
				&& _utils.filter(searchFor[1].constructor,[ String, Number, RegExp ])
			){
				var k = searchFor[0],v = searchFor[1];searchFor = {};searchFor[k] = v; searchFor = [searchFor]; }
		
		
			time('FIND');
		
			var _return = _utils.map.call(this,searchFor,function(item) {
				return _utils.filter(item,this.data);
			});

			timeEnd('FIND',_return.length + ' items, incl. duplicates');
			
			_return = Store(_return);
			
			_return.selector = searchFor;
			_return.makeUnique();			
			_return._prevObj = this;
			
		// Trigger events
			this.trigger('find',null,{
				target : this,
				params: searchFor,
				result: _return
			});	

			return _return;
		}
	};
	Store.item.init.prototype = Store.item;
})();

// =============================
// = Some additional functions =
// =============================

/*
 * Returns all keys found in data, once each 
 * Good for building an index with unique values
 */
Store.item.keys = function() {
	var r = [];
	this.each(function(item,index) {
		for (var key in item) {
			if( !r.join('\n').match( new RegExp('^'+Store.utils.regexp.escape(key)+'$','m') ) ) {
				r.push(key); } };
	});
	return r;
};
/*
 * Returns all values for the specified key, once each 
 */
Store.item.values = function(key,two) {	
	return map(this.data,function(item,index) {
		var value = item[key];
		if ( !r.join('\n').match( new RegExp('^'+Store.utils.regexp.escape(value)+'$','m') )  ) {
			r.push(value);
			return value ;
		}
	},function(r) { return r.reverse(); });
};
/*
 * Returns all values for the specified key,
 * which are found more than once
 * Good for checking for duplicates by specifying a
 * (should be) unique key
 */
Store.item.duplicates = function(key) {
	var i = {},r = [];
		
	this.each(function(item) {
		var v = item[key];
		if(typeof v === 'undefined' || (typeof v != 'string' && typeof v != 'number')) { return; }
		i[v] = i[v] || 0;
		i[v]++;
	});
	
	for(var n in i){if (i[n] > 1) { r.push(n); }};	
	
	return r;
};